﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Threading;
using System.Timers;
using System.Diagnostics;
using System.Windows.Threading;
using System.Windows.Media.Animation;

namespace Master
{
    public class Car : Image
    {
        /*****************
         * STATIC FIELDS *
         *****************/
        private static readonly Transform scaleTransform = new ScaleTransform(0.5, 0.5);
        private static readonly int sleepTime = 40;

        /*************************
         * DEPENDENCY PROPERTIES *
         *************************/
        public static readonly DependencyProperty directionProperty, positionProperty, orientationProperty, speedProperty;

        public Direction Direction
        {
            set { SetValue(directionProperty, value); }
            get { return (Direction)GetValue(directionProperty); }
        }

        public Point Position
        {
            set { SetValue(positionProperty, value); }
            get { return (Point)GetValue(positionProperty); }
        }

        public int Orientation
        {
            set { SetValue(orientationProperty, value); }
            get { return (int)GetValue(orientationProperty); }
        }

        public double Speed
        {
            set { SetValue(speedProperty, value); }
            get { return (double)GetValue(speedProperty); }
        }

        private static void positionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (Map.INSTANCE.ActualHeight == 0) return;
            Car thisCar = (Car)d; Rect bounds = thisCar.getTrueBounds();
            CarLeftMapEventArgs newEvent = null;
            if (bounds.X > Map.INSTANCE.ActualWidth && thisCar.Orientation >= 0 && thisCar.Orientation <= 180)
                newEvent = new CarLeftMapEventArgs(Car.CarLeftMapEvent, thisCar, Side.RIGHT);
            else if (bounds.X < 0 - bounds.Width && thisCar.Orientation >= 180)
                newEvent = new CarLeftMapEventArgs(Car.CarLeftMapEvent, thisCar, Side.LEFT);
            else if (bounds.Y > Map.INSTANCE.ActualHeight && thisCar.Orientation >= 90 && thisCar.Orientation <= 270)
                newEvent = new CarLeftMapEventArgs(Car.CarLeftMapEvent, thisCar, Side.BOTTOM);
            else if (bounds.Y < 0 - bounds.Height && (thisCar.Orientation <= 90 || thisCar.Orientation >= 270))
                newEvent = new CarLeftMapEventArgs(Car.CarLeftMapEvent, thisCar, Side.TOP);
            if (newEvent != null) thisCar.RaiseEvent(newEvent);
        }

        private static void orientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Car thisCar = (Car)d;
            TransformGroup transformGroup = new TransformGroup();
            transformGroup.Children.Add(new RotateTransform((int)e.NewValue));
            transformGroup.Children.Add(scaleTransform);
            thisCar.RenderTransform = transformGroup;
        }

        /**********
         * EVENTS *
         **********/
        public static readonly RoutedEvent CarLeftMapEvent; //--> GETS RAISED WHEN CAR LEAVES SCREEN

        public event CarLeftMapEventHandler CarLeftMap
        {
            add
            {
                AddHandler(Car.CarLeftMapEvent, value);
            }
            remove
            {
                RemoveHandler(Car.CarLeftMapEvent, value);
            }
        }

        /**********************
         * STATIC CONSTRUCTOR *
         **********************/
        static Car()
        {
            //DEPENDENCY PROPERTIES METADATA
            FrameworkPropertyMetadata positionPropertyMetadata = new FrameworkPropertyMetadata(new Point(-500, -500),
                new PropertyChangedCallback(Car.positionPropertyChanged));
            FrameworkPropertyMetadata orientationPropertyMetadata = new FrameworkPropertyMetadata(0,
                new PropertyChangedCallback(Car.orientationPropertyChanged));
            FrameworkPropertyMetadata speedPropertyMetadata = new FrameworkPropertyMetadata((double)10);
            //DEPENDENCY PROPERTIES
            directionProperty = DependencyProperty.Register("Direction", typeof(Direction), typeof(Car));
            positionProperty = DependencyProperty.Register("Position", typeof(Point), typeof(Car), positionPropertyMetadata);
            orientationProperty = DependencyProperty.Register("Orientation", typeof(int), typeof(Car), orientationPropertyMetadata);
            speedProperty = DependencyProperty.Register("Speed", typeof(double), typeof(Car), speedPropertyMetadata);
            //EVENTS
            Car.CarLeftMapEvent = EventManager.RegisterRoutedEvent("CarLeftMap",
                RoutingStrategy.Direct, typeof(CarLeftMapEventHandler), typeof(Car));
        }

        /**********
         * FIELDS *
         **********/
        private Thread driveThread = null;
        private Rect trueBounds;

        /***************
         * CONSTRUCTOR *
         ***************/
        public Car()
        {
            initCarImage();
            bindPosition();
        }

        /******************
         * INITIALIZATION *
         ******************/
        private void initCarImage()
        {
            Source = new BitmapImage(new Uri(@"/Master;component/Images/simple-travel-car-top_view.png", UriKind.Relative));
            RenderTransformOrigin = new Point(0.5, 0.5);
            TransformGroup transformGroup = new TransformGroup();
            transformGroup.Children.Add(new RotateTransform(Orientation));
            transformGroup.Children.Add(scaleTransform);
            RenderTransform = transformGroup;
        }

        private void bindPosition()
        {
            Binding Xbinding = new Binding();
            Xbinding.Source = this;
            Xbinding.Path = new PropertyPath("Position.X");
            Xbinding.Mode = BindingMode.TwoWay;
            this.SetBinding(Map.LeftProperty, Xbinding);

            Binding Ybinding = new Binding();
            Ybinding.Source = this;
            Ybinding.Path = new PropertyPath("Position.Y");
            Ybinding.Mode = BindingMode.TwoWay;
            this.SetBinding(Map.TopProperty, Ybinding);
        }

        /***********
         * METHODS *
         ***********/
        public void updateTrueBounds(Rect newBounds)
        {
            trueBounds = newBounds;
        }

        public Rect getTrueBounds()
        {
            return trueBounds;
        }

        public Point getFrontBumperPoint()
        {
            if (Orientation == 0) return new Point((trueBounds.Left + trueBounds.Right) / 2, trueBounds.Top);
            else if (Orientation < 90) return trueBounds.TopRight;
            else if (Orientation == 90) return new Point(trueBounds.Right, (trueBounds.Top + trueBounds.Bottom) / 2);
            else if (Orientation < 180) return trueBounds.BottomRight;
            else if (Orientation == 180) return new Point((trueBounds.Left + trueBounds.Right) / 2, trueBounds.Bottom);
            else if (Orientation < 270) return trueBounds.BottomLeft;
            else if (Orientation == 270) return new Point(trueBounds.Left, (trueBounds.Top + trueBounds.Bottom) / 2);
            else return trueBounds.TopLeft;
        }

        public Point getRearBumperPoint()
        {
            if (Orientation == 0) return new Point((trueBounds.Left + trueBounds.Right) / 2, trueBounds.Bottom);
            else if (Orientation < 90) return trueBounds.BottomLeft;
            else if (Orientation == 90) return new Point(trueBounds.Left, (trueBounds.Top + trueBounds.Bottom) / 2);
            else if (Orientation < 180) return trueBounds.TopLeft;
            else if (Orientation == 180) return new Point((trueBounds.Left + trueBounds.Right) / 2, trueBounds.Top);
            else if (Orientation < 270) return trueBounds.TopRight;
            else if (Orientation == 270) return new Point(trueBounds.Right, (trueBounds.Top + trueBounds.Bottom) / 2);
            else return trueBounds.BottomRight;
        }

        public Point getTrueCenter()
        {
            return new Point((trueBounds.Left + trueBounds.Right) / 2, (trueBounds.Top + trueBounds.Bottom) / 2);
        }

        public void startDriving()
        {
            driveThread = new Thread(drive);
            driveThread.Start();
        }

        public void stopDriving()
        {
            if (driveThread == null) return;
            driveThread.Abort();
            driveThread = null;
        }

        private static Point newPosition(Point oldPoint, int orientation, double speed)
        {
            return new Point(oldPoint.X + (speed * Math.Sin(degreeToRadian(orientation))),
                oldPoint.Y - (speed * Math.Cos(degreeToRadian(orientation))));
        }

        private static double degreeToRadian(double angle)
        {
            return Math.PI * angle / 180.0;
        }

        /****************
         * EXPERIMENTAL *
         ****************/
        private TrafficLight nextTrafficLight;
        private TrafficMarshal trafficMarshal;
        private Direction nextDirection;

        private Point nextDestination;
        private Boolean hasDestination, watchTrafficMarshal, onIntersection;
        private double totalDistance, xDistance, yDistance;

        private int oldOrientation, newOrientation;
        private double maxSpeed, effectiveSpeed;

        private void initDrive()
        {
            nextDirection = Toolkit.RandomEnum<Direction>();
            nextDestination = new Point();
            hasDestination = false;
            watchTrafficMarshal = false;
            onIntersection = false;
            maxSpeed = -1;
        }

        private void drive()
        {
            try
            {
                initDrive();
                while (true)
                {
                    DateTime startTime = DateTime.Now; //get starttime
                    Map.INSTANCE.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(doDrive)); //drive
                    updateBounds(); //update bounds
                    DateTime stopTime = DateTime.Now; //get endtime
                    Thread.Sleep(TimeSpan.FromMilliseconds(sleepTime) - (startTime - stopTime)); //sleep
                }
            }
            catch (ThreadAbortException e)
            {
                Trace.WriteLine("Car was stopped :(");
            }
        }

        private void doDrive()
        {
            updateDestination();
            effectiveSpeed = (maxSpeed != -1 && maxSpeed < Speed) ? maxSpeed : Speed;
            checkTrafficLight(); //as of here, check if effectiveSpeed == 0
            checkTrafficMarshal();
            checkCars();
            changePosition();
        }

        private void resetCarState()
        {
            hasDestination = false;
            nextTrafficLight = null;
            if (watchTrafficMarshal)
                trafficMarshal.removeCar(this);
            watchTrafficMarshal = false;
            trafficMarshal = null;
            onIntersection = false;
        }

        private void updateDestination()
        {
            if (hasDestination)
            {
                Rect bounds = getTrueBounds();
                Point checkPoint = new Point();
                checkPoint.X = getTrueCenter().X - (Speed / 2);
                checkPoint.Y = getTrueCenter().Y - (Speed / 2);
                if (Toolkit.elementIsAtPoint(checkPoint, Speed, Speed, nextDestination) ||  //check if we have reached our next destination (checks if center of car is at destination-point)
                    (Map.INSTANCE.getInterSectionAt(getFrontBumperPoint()) != null && !onIntersection) ||  //check if we have reached a new intersection (checks if bumper of car is at intersection)
                    (Map.INSTANCE.getInterSectionAt(getFrontBumperPoint()) == null && Map.INSTANCE.getInterSectionAt(getTrueCenter()) == null 
                        && Map.INSTANCE.getLaneAt(getRearBumperPoint()) != null && onIntersection)) //extra check in case the car didn't hit the destination point (this can happen on badly shaped intersections)
                    resetCarState();
                else return;
            }
            if (!getDestinationFromIntersection())
            {
                Lane newLane = getDestinationFromLane();
                if (newLane != null) snapToLane(newLane);
            }
            oldOrientation = Orientation;
        }

        private Boolean getDestinationFromIntersection()
        {
            InterSection interSection = Map.INSTANCE.getInterSectionAt(getFrontBumperPoint());
            if (interSection != null)
            {
                hasDestination = true;
                Lane destinationLane = getExitLane(interSection.getRoadsBySide(findNewSide()));
                updateDestination(destinationLane.getStartPoint());
                maxSpeed = destinationLane.MaxSpeed;
                newOrientation = destinationLane.Orientation;
                trafficMarshal = interSection.getTrafficMarshal();
                trafficMarshal.registerCar(new DirectionData(Orientation, nextDirection), this);
                watchTrafficMarshal = true;
                onIntersection = true;
                return true;
            }
            return false;
        }

        private Lane getExitLane(List<Road> roads)
        {
            foreach (Road road in roads) foreach (Lane lane in road.getLanes())
                {
                    if (lane.Type == LaneType.EXIT)
                        return lane;
                }
            throw new InvalidOperationException();
        }

        private Side findNewSide()
        {
            if ((Orientation == 90 && nextDirection == Direction.LEFT) ||
                (Orientation == 270 && nextDirection == Direction.RIGHT) ||
                (Orientation == 0 && nextDirection == Direction.FORWARD))
                return Side.TOP;
            else if ((Orientation == 90 && nextDirection == Direction.RIGHT) ||
                (Orientation == 270 && nextDirection == Direction.LEFT) ||
                (Orientation == 180 && nextDirection == Direction.FORWARD))
                return Side.BOTTOM;
            else if ((Orientation == 0 && nextDirection == Direction.RIGHT) ||
                (Orientation == 180 && nextDirection == Direction.LEFT) ||
                (Orientation == 90 && nextDirection == Direction.FORWARD))
                return Side.RIGHT;
            else return Side.LEFT;
        }

        private Lane getDestinationFromLane()
        {
            Lane lane = Map.INSTANCE.getLaneAt(getFrontBumperPoint());
            if (lane != null)
            {
                hasDestination = true;
                updateDestination(lane.getDestinationPoint());
                maxSpeed = lane.MaxSpeed;
                newOrientation = lane.Orientation;
                if (lane.hasTrafficLight())
                    nextTrafficLight = lane.trafficLight;
                nextDirection = Toolkit.RandomEnum<Direction>();
            }
            return lane;
        }

        private void updateDestination(Point newDestination)
        {
            nextDestination = newDestination;
            Point centerPoint = getTrueCenter();
            xDistance = getXDistance(centerPoint, newDestination);
            yDistance = getYDistance(centerPoint, newDestination);
            totalDistance = xDistance + yDistance;
        }

        private void checkTrafficLight()
        {
            if (nextTrafficLight == null) return;
            Point trafficLightLocation = Toolkit.getPointRelativeToMap(nextTrafficLight);
            Point carCenterLocation = getTrueCenter();
            double distance = 0; double carSize = 0;
            if (Orientation == 0 || Orientation == 180)
            {
                distance = trafficLightLocation.Y > carCenterLocation.Y ? trafficLightLocation.Y - carCenterLocation.Y : carCenterLocation.Y - trafficLightLocation.Y;
                carSize = getTrueBounds().Height;
            }
            else
            {
                distance = trafficLightLocation.X > carCenterLocation.X ? trafficLightLocation.X - carCenterLocation.X : carCenterLocation.X - trafficLightLocation.X;
                carSize = getTrueBounds().Width;
            }
            if (distance <= (carSize + maxSpeed) && distance >= carSize && nextTrafficLight.Status == TrafficLightStatus.RED)
                effectiveSpeed = 0;
        }

        private void checkTrafficMarshal()
        {
            if (!watchTrafficMarshal || effectiveSpeed <= 0) 
                return;
            if (!trafficMarshal.canMoveForward(this))
                effectiveSpeed = 0;
        }

        private void checkCars()
        {
            if (effectiveSpeed == 0) return; //already stopped, no need to check for cars
            double distance = 0;
            if (Orientation == 270 || Orientation == 90)
                distance = getTrueBounds().Width;
            else if (Orientation == 0 || Orientation == 180)
                distance = getTrueBounds().Height;
            else return;
            if (Map.INSTANCE.getCarAtPoint(newPosition(getTrueCenter(), Orientation, distance)) != null)
                effectiveSpeed = 0;
        }

        private void snapToLane(Lane lane)
        {
            Point currentCenterPoint = getTrueCenter();
            Point wantedCenterPoint = new Point(currentCenterPoint.X, currentCenterPoint.Y);
            if (lane.Orientation == 90 || lane.Orientation == 270)
            {
                Point laneLocation = Toolkit.getPointRelativeToMap(lane);
                wantedCenterPoint.Y = ((laneLocation.Y * 2) + lane.ActualHeight) / 2;
            }
            else
            {
                Point laneLocation = Toolkit.getPointRelativeToMap(lane);
                wantedCenterPoint.X = ((laneLocation.X * 2) + lane.ActualWidth) / 2;
            }
            Orientation = lane.Orientation;
            Position = new Point(Position.X + (wantedCenterPoint.X - currentCenterPoint.X), Position.Y + (wantedCenterPoint.Y - currentCenterPoint.Y));
        }

        private void changePosition()
        {
            if (effectiveSpeed <= 0) return;
            if (!hasDestination || newOrientation == oldOrientation) //no destination set or new destination is straight ahead
                Position = newPosition(Position, Orientation, effectiveSpeed);
            else
                moveTowardDestination();
        }

        private double getXDistance(Point a, Point b)
        {
            double xDistance = a.X - b.X;
            if (xDistance < 0) xDistance += -1;
            return xDistance;
        }

        private double getYDistance(Point a, Point b)
        {
            double yDistance = a.Y - b.Y;
            if (yDistance < 0) yDistance += -1;
            return yDistance;
        }

        private double getTotalDistance(Point a, Point b)
        {
            return getXDistance(a, b) + getYDistance(a, b);
        }

        private CircleEase ease = new CircleEase();

        private void moveTowardDestination()
        {
            double xSpeed = 0, ySpeed = 0, currentDistance = 0, distanceEaseResult = 0;
            if (oldOrientation == 180 || oldOrientation == 0)
            {
                currentDistance = getYDistance(getTrueCenter(), nextDestination);
                distanceEaseResult = ease.Ease(currentDistance / yDistance);
            }
            else
            {
                currentDistance = getXDistance(getTrueCenter(), nextDestination);
                distanceEaseResult = ease.Ease(currentDistance / xDistance);
            }
            xSpeed = getXSpeed(distanceEaseResult);
            ySpeed = getYSpeed(distanceEaseResult);
            generateNewOrientation(distanceEaseResult);
            Position = new Point(Position.X + xSpeed, Position.Y + ySpeed);
        }

        private double getXSpeed(double easeResult)
        {
            if (oldOrientation == 90)
                return effectiveSpeed * easeResult;
            if (oldOrientation == 270)
                return (effectiveSpeed * easeResult) * -1;
            if (newOrientation == 90)
                return effectiveSpeed * (1 - easeResult);
            else
                return (effectiveSpeed * (1 - easeResult)) * -1;
        }

        private double getYSpeed(double easeResult)
        {
            if (oldOrientation == 0) //car was moving towards the top of the screen
                return (effectiveSpeed * easeResult) * -1;
            if (oldOrientation == 180) //car was moving towards the bottom of the screen
                return effectiveSpeed * easeResult;
            if (newOrientation == 0) //car is turning towards the top of the screen
                return (effectiveSpeed * (1 - easeResult)) * -1;
            else //car is turning towards the bottom of the screen
                return (effectiveSpeed * (1 - easeResult));
        }

        private void generateNewOrientation(double easeResult)
        {
            if (nextDirection == Direction.RIGHT)
                Orientation = Toolkit.alterOrientationBy(oldOrientation, (int)(90 * (1 - easeResult)));
            else
                Orientation = Toolkit.alterOrientationBy(oldOrientation, (int)(-90 * (1 - easeResult)));
        }

        //force re-rendering (--> this ACTUALLY updates the position and re-draws the car with the new data) and draw hitbox
        private void updateBounds()
        {
            this.Dispatcher.Invoke(DispatcherPriority.Render,
                new Action(delegate()
                {
                    updateTrueBounds(getNewBounds(this));
                }
            ));
        }

        //drawhitbox & everything related to it are prototypes to try and get the true coordinates and dimensions of the transformed car
        private Line[] lines, lines2;

        private static Rect getNewBounds(Car thisCar)
        {
            Rect bounds = Toolkit.getBounds(thisCar);
            drawHitBox(thisCar, bounds);
            /*/-- THIS SECTION IS OPTIONAL AND WILL BE REMOVED AT A LATER STAGE --
             drawHitBox(thisCar, bounds);
             drawShitBox(thisCar);
             //--------------------- END OF OPTIONAL SECTION ---------------------*/

            return bounds;
        }

        //HITBOX = actual size (after applying transformations)
        private static void drawHitBox(Car thisCar, Rect bounds)
        {
            if (thisCar.lines != null) foreach (Line line in thisCar.lines) Map.INSTANCE.Children.Remove(line);
            thisCar.lines = new Line[4];
            thisCar.lines[0] = createLine(bounds.TopLeft, bounds.TopRight, false);
            thisCar.lines[1] = createLine(bounds.TopRight, bounds.BottomRight, false);
            thisCar.lines[2] = createLine(bounds.BottomRight, bounds.BottomLeft, false);
            thisCar.lines[3] = createLine(bounds.BottomLeft, bounds.TopLeft, false);
        }

        //SHITBOX = 'WPF' size (dimensions according to WPF, these do not consider transformations)
        private static void drawShitBox(Car thisCar)
        {
            Point currentPoint = thisCar.Position;
            Rect bounds = new Rect(currentPoint.X, currentPoint.Y, thisCar.ActualWidth, thisCar.ActualHeight);

            if (thisCar.lines2 != null) foreach (Line line in thisCar.lines2) Map.INSTANCE.Children.Remove(line);
            thisCar.lines2 = new Line[4];
            thisCar.lines2[0] = createLine(bounds.TopLeft, bounds.TopRight, true);
            thisCar.lines2[1] = createLine(bounds.TopRight, bounds.BottomRight, true);
            thisCar.lines2[2] = createLine(bounds.BottomRight, bounds.BottomLeft, true);
            thisCar.lines2[3] = createLine(bounds.BottomLeft, bounds.TopLeft, true);
        }

        private static Line createLine(Point startPoint, Point endPoint, Boolean isShitBox)
        {
            Line myLine = new Line();
            myLine.Stroke = (isShitBox) ? System.Windows.Media.Brushes.SaddleBrown : System.Windows.Media.Brushes.LightSteelBlue;
            myLine.X1 = startPoint.X; myLine.X2 = endPoint.X;
            myLine.Y1 = startPoint.Y; myLine.Y2 = endPoint.Y;
            myLine.StrokeThickness = 2;
            Map.INSTANCE.Children.Add(myLine);
            return myLine;
        }
    }
}